/* This file is part of Eternity II Editor. * * Eternity II Editor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Eternity II Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Eternity II Editor. If not, see <http://www.gnu.org/licenses/>. * * Eternity II Editor project is hosted on SourceForge: * http://sourceforge.net/projects/eternityii/ * and maintained by Yannick Kirschhoffer <alcibiade@alcibiade.org> */ package org.alcibiade.eternity.editor.gui; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.dnd.InvalidDnDOperationException; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.AffineTransform; import java.awt.geom.Dimension2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import javax.swing.JComponent; import org.alcibiade.eternity.editor.gui.transfer.DataFlavors; import org.alcibiade.eternity.editor.gui.transfer.TransferablePattern; import org.alcibiade.eternity.editor.gui.transfer.TransferableQuadModel; import org.alcibiade.eternity.editor.model.Pattern; import org.alcibiade.eternity.editor.model.QuadModel; import org.alcibiade.eternity.editor.model.QuadObserver; public class QuadView extends JComponent implements QuadObserver, MouseListener, MouseWheelListener, MouseMotionListener, DragGestureListener, DragSourceListener, DropTargetListener, KeyListener { private static final int CURSOR_COMPLETE_QUAD = 5; private static final Color COLOR_UNLOCKED = Color.BLACK; private static final Color COLOR_LOCKED = Color.YELLOW; private static final Color COLOR_ERROR = Color.RED; private static final Color COLOR_CURSOR = new Color(0xF2F8FD); private static final long serialVersionUID = 1L; private QuadModel model; private boolean editable; private boolean showPatternIds; private boolean[] error = new boolean[4]; private Integer cursor = null; private DragSource dragSource; public QuadView(QuadModel model) { this.model = model; this.model.addQuadObserver(this); setEditable(true); setShowPatternIds(false); addMouseListener(this); addMouseWheelListener(this); addMouseMotionListener(this); addKeyListener(this); dragSource = DragSource.getDefaultDragSource(); dragSource.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, this); setDropTarget(new DropTarget(this, this)); setFocusable(true); setError(QuadModel.DIR_NORTH, false); setError(QuadModel.DIR_EAST, false); setError(QuadModel.DIR_WEST, false); setError(QuadModel.DIR_SOUTH, false); } public void setEditable(boolean editable) { this.editable = editable; } public void setShowPatternIds(boolean show) { this.showPatternIds = show; repaint(); } public boolean getShowPatternIds() { return showPatternIds; } public void setError(int direction, boolean error) { if (error != this.error[direction]) { this.error[direction] = error; repaint(); } } public QuadModel getModel() { return model; } public void quadUpdated() { repaint(); } @Override public void paint(Graphics graphics) { Dimension2D size = getSize(); double wd = size.getWidth(); double ht = size.getHeight(); double minsize = Math.min(wd, ht); Graphics2D g2d = (Graphics2D) graphics; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Fill background // Draw the patterns double dx = wd / 2; double dy = ht / 2; Shape prev_clip = g2d.getClip(); g2d.clip(new Rectangle2D.Double(0, 0, wd - 0.5, ht - 0.5)); AffineTransform prev_trx = g2d.getTransform(); g2d.translate(0, -dy); model.getPattern(QuadModel.DIR_NORTH).paint(g2d, size); g2d.translate(dx, dy); model.getPattern(QuadModel.DIR_EAST).paint(g2d, size); g2d.translate(-dx, dy); model.getPattern(QuadModel.DIR_SOUTH).paint(g2d, size); g2d.translate(-dx, -dy); model.getPattern(QuadModel.DIR_WEST).paint(g2d, size); g2d.setTransform(prev_trx); g2d.setClip(prev_clip); // Draw the separation lines if (model.isLocked()) { g2d.setColor(COLOR_LOCKED); } else { g2d.setColor(COLOR_UNLOCKED); } g2d.setStroke(new BasicStroke((float) (minsize / 50.))); g2d.draw(new Rectangle2D.Double(0, 0, wd, ht)); g2d.draw(new Line2D.Double(0, 0, wd, ht)); g2d.draw(new Line2D.Double(wd, 0, 0, ht)); if (cursor != null && editable) { g2d.setColor(COLOR_CURSOR); if (cursor == CURSOR_COMPLETE_QUAD) { drawTriangle(g2d, dx, dy, wd, ht, 0); drawTriangle(g2d, dx, dy, wd, ht, 1); drawTriangle(g2d, dx, dy, wd, ht, 2); drawTriangle(g2d, dx, dy, wd, ht, 3); } else { drawTriangle(g2d, dx, dy, wd, ht, cursor); } } g2d.setColor(COLOR_ERROR); g2d.setStroke(new BasicStroke((float) (minsize / 20.))); for (int direction = 0; direction < 4; direction++) { if (error[direction]) { drawTriangle(g2d, dx, dy, wd, ht, direction); } } if (showPatternIds) { Font font = new Font("SansSerif", Font.PLAIN, (int) (dy / 3.)); g2d.setFont(font); g2d.setColor(Color.BLACK); drawPatternIds(g2d, dx, 1, dy, 1); g2d.setColor(Color.WHITE); drawPatternIds(g2d, dx, 0, dy, 0); } } private void drawPatternIds(Graphics2D g2d, double dx, double ox, double dy, double oy) { drawPatternId(g2d, Integer.toString(model.getPattern(QuadModel.DIR_NORTH).getCode()), dx + ox, 0.3 * dy + oy); drawPatternId(g2d, Integer.toString(model.getPattern(QuadModel.DIR_SOUTH).getCode()), dx + ox, 1.7 * dy + oy); drawPatternId(g2d, Integer.toString(model.getPattern(QuadModel.DIR_EAST).getCode()), 1.7 * dx + ox, dy + oy); drawPatternId(g2d, Integer.toString(model.getPattern(QuadModel.DIR_WEST).getCode()), 0.3 * dx + ox, dy + oy); } private void drawPatternId(Graphics2D g2d, String text, double x, double y) { Rectangle2D bounds = g2d.getFont().getStringBounds(text, g2d.getFontRenderContext()); g2d.drawString(text, (float) (x - bounds.getWidth() / 2), (float) (y + bounds.getHeight() / 3)); } private void drawTriangle(Graphics2D g2d, double dx, double dy, double wd, double ht, int direction) { GeneralPath triangle = new GeneralPath(); triangle.moveTo((float) (dx), (float) (dy)); if (direction == QuadModel.DIR_NORTH) { triangle.lineTo((0), (0)); triangle.lineTo((float) (wd), (0)); } else if (direction == QuadModel.DIR_EAST) { triangle.lineTo((float) (wd), (0)); triangle.lineTo((float) (wd), (float) (ht)); } else if (direction == QuadModel.DIR_SOUTH) { triangle.lineTo((float) (wd), (float) (ht)); triangle.lineTo((0), (float) (ht)); } else if (direction == QuadModel.DIR_WEST) { triangle.lineTo((0), (float) (ht)); triangle.lineTo((0), (0)); } triangle.lineTo((float) (dx), (float) (dy)); g2d.draw(triangle); } /** * MouseListener */ public void mouseReleased(MouseEvent event) { if (editable) { switch (event.getButton()) { case MouseEvent.BUTTON1: Point2D mouse = event.getPoint(); model.rotatePattern(computeDirection(mouse)); break; case MouseEvent.BUTTON2: // Switch the lock status model.setLocked(!model.isLocked()); } } } public void mouseEntered(MouseEvent arg0) { if (!hasFocus()) { requestFocus(); } } public void mouseExited(MouseEvent arg0) { this.cursor = null; repaint(); } public void mouseClicked(MouseEvent arg0) { // Do nothing } public void mousePressed(MouseEvent arg0) { // Do nothing } /** * MouseMotionListener */ public void mouseDragged(MouseEvent e) { // Do nothing } public void mouseMoved(MouseEvent e) { Point point = e.getPoint(); handleMouseHover(point, e.isShiftDown()); } /** * MouseWheelListener */ public void mouseWheelMoved(MouseWheelEvent e) { if (editable) { if (e.getWheelRotation() < 0) { model.rotateClockwise(); } else { model.rotateCounterclockwise(); } } } /** * KeyListener */ public void keyPressed(KeyEvent e) { if (editable && cursor != null) { switch (e.getKeyCode()) { case KeyEvent.VK_RIGHT: if (cursor == CURSOR_COMPLETE_QUAD) { model.rotatePattern(0, true); model.rotatePattern(1, true); model.rotatePattern(2, true); model.rotatePattern(3, true); } else { model.rotatePattern(cursor, true); } break; case KeyEvent.VK_LEFT: if (cursor == CURSOR_COMPLETE_QUAD) { model.rotatePattern(0, false); model.rotatePattern(1, false); model.rotatePattern(2, false); model.rotatePattern(3, false); } else { model.rotatePattern(cursor, false); } break; } handleMouseHover(null, e.isShiftDown()); repaint(); } } public void keyReleased(KeyEvent e) { handleMouseHover(null, e.isShiftDown()); } public void keyTyped(KeyEvent e) { // Do nothing } /** * DragGestureListener */ public void dragGestureRecognized(DragGestureEvent dge) { try { boolean wholeQuad = dge.getTriggerEvent().isShiftDown(); Transferable transferable; if (wholeQuad) { transferable = new TransferableQuadModel(model); } else { Pattern pattern = model.getPattern(computeDirection(dge.getDragOrigin())); transferable = new TransferablePattern(pattern); } dge.startDrag(DragSource.DefaultCopyNoDrop, transferable, this); } catch (InvalidDnDOperationException idoe) { System.err.println(idoe); } } /** * DragSourceListener */ public void dragDropEnd(DragSourceDropEvent dsde) { cursor = null; repaint(); } public void dragEnter(DragSourceDragEvent dsde) { // Do nothing } public void dragExit(DragSourceEvent dse) { // Do nothing } public void dragOver(DragSourceDragEvent dsde) { // Do nothing } public void dropActionChanged(DragSourceDragEvent dsde) { // Do nothing } public void dragEnter(DropTargetDragEvent dtde) { // Do nothing } public void dragExit(DropTargetEvent dte) { // Do nothing } public void dragOver(DropTargetDragEvent dtde) { // Do nothing } /** * DropTargetListener */ public void drop(DropTargetDropEvent dtde) { if (editable) { try { Transferable transferable = dtde.getTransferable(); Point2D point = dtde.getLocation(); if (transferable.isDataFlavorSupported(DataFlavors.quadModelDataFlavor)) { QuadModel sourceQuadModel = (QuadModel) transferable .getTransferData(DataFlavors.quadModelDataFlavor); model.swap(sourceQuadModel); } else if (transferable.isDataFlavorSupported(DataFlavors.patternDataFlavor)) { Pattern pattern = (Pattern) transferable .getTransferData(DataFlavors.patternDataFlavor); model.setPattern(computeDirection(point), pattern); } handleMouseHover(point, false); if (!hasFocus()) { requestFocus(); } } catch (UnsupportedFlavorException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NumberFormatException e) { e.printStackTrace(); } } } public void dropActionChanged(DropTargetDragEvent dtde) { // Do nothing } private Point2D lastPoint = null; /** * Handle mouse hovering over this component and adjust cursor accordingly. * * @param point * Mouse coordinates */ private void handleMouseHover(Point2D point, boolean fullQuad) { if (point == null) { point = lastPoint; } else { lastPoint = point; } int cursor; if (fullQuad) { cursor = CURSOR_COMPLETE_QUAD; } else { cursor = computeDirection(point); } if (this.cursor == null || cursor != this.cursor) { this.cursor = cursor; repaint(); } } /** * Compute the quad direction from the mouse coordinates. * * @param point * @return */ private int computeDirection(Point2D point) { int direction; Dimension2D size = getSize(); Point2D relative = new Point2D.Double(point.getX() / size.getWidth(), point.getY() / size.getHeight()); boolean ne = relative.getX() > relative.getY(); boolean nw = relative.getX() + relative.getY() <= 1; if (ne) { if (nw) { direction = QuadModel.DIR_NORTH; } else { direction = QuadModel.DIR_EAST; } } else { if (nw) { direction = QuadModel.DIR_WEST; } else { direction = QuadModel.DIR_SOUTH; } } return direction; } }